﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.ComponentModel;
using System.Threading;

namespace Wheels
{
    public static class ConversionUtility
    {
#pragma warning disable 1591

        #region Change Type

        static object ChangeTypeNoCheck(this object value, Type sourceType, Type conversionType, IFormatProvider provider, ConversionOptions options)
        {
            bool successed;
            object result = null;

            if (provider == null)
                provider = Thread.CurrentThread.CurrentCulture;

            switch (options & ConversionOptions.MethodMask)
            {
                case ConversionOptions.OperatorOnly:
                    successed = value.TryOperator(sourceType, conversionType, options, ref result);
                    break;
                case ConversionOptions.ConverterOnly:
                    successed = value.TryConverter(sourceType, conversionType, provider as CultureInfo, options, ref result);
                    break;
                case ConversionOptions.ConverterThenOperator:
                    successed = value.TryConverter(sourceType, conversionType, provider as CultureInfo, options, ref result)
                        || value.TryOperator(sourceType, conversionType, options, ref result);
                    break;
                default:
                    successed = value.TryOperator(sourceType, conversionType, options, ref result)
                        || value.TryConverter(sourceType, conversionType, provider as CultureInfo, options, ref result);
                    break;
            }

            if (successed)
                return result;

            if (ConversionOptions.Zero == (options & ConversionOptions.UseIConvertible))
                throw new InvalidCastException();

            var convertible = value as IConvertible;
            if (convertible == null)
                throw new InvalidCastException();

            return convertible.ToType(conversionType, provider);
        }

        #region TryOperator

        private static bool TryOperator(this object value, Type sourceType, Type conversionType, ConversionOptions options, ref object result)
        {
            switch (options & ConversionOptions.LookupMask)
            {
                case ConversionOptions.SourceOnly:
                    return value.TrySourceOperator(sourceType, conversionType, ref result);
                case ConversionOptions.TargetOnly:
                    return value.TryTargetOperator(sourceType, conversionType, ref result);
                case ConversionOptions.SourceThenTarget:
                    return value.TrySourceOperator(sourceType, conversionType, ref result)
                        || value.TryTargetOperator(sourceType, conversionType, ref result);
                default:
                    return value.TryTargetOperator(sourceType, conversionType, ref result)
                        || value.TrySourceOperator(sourceType, conversionType, ref result);
            }
        }
        private static bool TrySourceOperator(this object value, Type sourceType, Type conversionType, ref object result)
        {
            foreach (var mi in sourceType.GetMethods())
            {
                if (mi.IsSpecialName && (mi.Name == "op_Implicit" || mi.Name == "op_Explicit") && mi.ReturnType == conversionType)
                {
                    var @params = mi.GetParameters();
                    if (@params.Length == 1)
                    {
                        if (@params[1].ParameterType == sourceType)
                        {
                            mi.Invoke(null, new[] { value });
                            return true;
                        }
                    }
                }
            }

            return false;
        }
        private static bool TryTargetOperator(this object value, Type sourceType, Type conversionType, ref object result)
        {
            foreach (var mi in conversionType.GetMethods())
            {
                if (mi.IsSpecialName && (mi.Name == "op_Implicit" || mi.Name == "op_Explicit") && mi.ReturnType == conversionType)
                {
                    var @params = mi.GetParameters();
                    if (@params.Length == 1)
                    {
                        if (@params[1].ParameterType == sourceType)
                        {
                            mi.Invoke(null, new[] { value });
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        #endregion

        #region TryConverter

        private static bool TryConverter(this object value, Type sourceType, Type conversionType, CultureInfo culture, ConversionOptions options, ref object result)
        {
            switch (options & ConversionOptions.LookupMask)
            {
                case ConversionOptions.SourceOnly:
                    return value.TrySourceConverter(sourceType, conversionType, culture, ref result);
                case ConversionOptions.TargetOnly:
                    return value.TryTargetConverter(sourceType, conversionType, culture, ref result);
                case ConversionOptions.SourceThenTarget:
                    return value.TrySourceConverter(sourceType, conversionType, culture, ref result)
                        || value.TryTargetConverter(sourceType, conversionType, culture, ref result);
                default:
                    return value.TryTargetConverter(sourceType, conversionType, culture, ref result)
                        || value.TrySourceConverter(sourceType, conversionType, culture, ref result);
            }
        }
        private static bool TrySourceConverter(this object value, Type sourceType, Type conversionType, CultureInfo culture, ref object result)
        {
            TypeConverter converter;

            if (value != null)
            {
                converter = TypeDescriptor.GetConverter(value);
                if (converter.CanConvertTo(conversionType))
                {
                    result = converter.ConvertTo(null, culture, value, conversionType);
                    return true;
                }
            }
            else if (sourceType != Types.Object)
            {
                converter = TypeDescriptor.GetConverter(sourceType);
                if (converter.CanConvertTo(conversionType))
                {
                    result = converter.ConvertTo(null, culture, value, conversionType);
                    return true;
                }
            }

            return false;
        }
        private static bool TryTargetConverter(this object value, Type sourceType, Type conversionType, CultureInfo culture, ref object result)
        {
            TypeConverter converter;

            converter = TypeDescriptor.GetConverter(conversionType);
            if (converter.CanConvertFrom(sourceType))
            {
                result = converter.ConvertFrom(null, culture, value);
                return true;
            }

            return false;
        }

        #endregion

        #endregion

        public static T To<T>(this object value, Type sourceType, IFormatProvider provider = null, ConversionOptions options = ConversionOptions.Default)
        {
            if (sourceType == null)
                throw NewException.ArgumentNull(() => sourceType);

            return (T)value.ChangeTypeNoCheck(sourceType, typeof(T), provider, options);
        }

        public static T To<T>(this object value, IFormatProvider provider, ConversionOptions options = ConversionOptions.Default)
        {
            if (value == null)
                return (T)value.ChangeTypeNoCheck(Types.Object, typeof(T), provider, options);
            else
                return (T)value.ChangeTypeNoCheck(value.GetType(), typeof(T), provider, options);
        }

        public static object ChangeType<T>(this T value, Type conversionType, IFormatProvider provider = null, ConversionOptions options = ConversionOptions.Default)
        {
            return value.ChangeTypeNoCheck(typeof(T), conversionType, provider, options);
        }
    }
}
